/**
 * 
 */
package edu.towson.cosc455.JohnCarloSalter.compiler;

import java.util.ArrayList;
import java.io.PrintWriter;

/**
 * John Carlo Salter
 * Semantic Analyzer for our Lolcode Markdown Compiler.
 * SemanticAnalyzer takes in a Lolcode parse tree
 * and converts it to an HTML parse tree.
 */
public class SemanticAnalyzer {
	public ArrayList<String> parseTree;
	public int currentToken;
	public ArrayList<String> scopeStack; //Will use this to handle scoping with variables.
										 //We will also use this to differentiate between tags that have the same value in Lolcode but not HTML.
	public int scopeStackTop;	//Top of the stack should point to last element of scopeStack.
	
	
	private final String SCOPE_BUFFER = "###"; //Arbitrary as long as it can't equal a variable name.
									  		   //We use the scopeBuffer to separate different sections of 
											   //scope in scopeStack. When leaving a section of scope, we
											   //remove from the stack until we've removed two scope buffers.
	
	//Scope Indicators
	//These Strings will serve as flags to indicate which type of block is ending.
	final String SIhead = "SIhead";
	final String SIparagraph = "SIparagraph";
	final String SIlist = "SIlist";
	final String SItitle = "SItitle";
	final String SIbold = "SIbold";
	final String SIitalics = "SIitalics";
	final String SIlist_item = "SIlist_item";
	final String SIsound = "SIsound";
	final String SIvideo = "SIvideo";
	
	//Constructor:  Pass this a Lolcode parseTree.
	public SemanticAnalyzer(ArrayList<String> tree) {
		parseTree = new ArrayList<String>(tree);
		scopeStack = new ArrayList<String>();
		currentToken = 0;
		scopeStackTop = -1; //-1 indicates empty stack.
	}
	
	//Overall method to run the semantic analyzer as our Compiler would like.
	public void run(PrintWriter outFile) {
		convertTreeToHTML();
		generateOutputFile(outFile);
	}
	
	//Method to turn a parse tree into a file.
	public void generateOutputFile(PrintWriter outFile) {
		for(int i=0; i<parseTree.size(); i++) {
			outFile.print(parseTree.get(i));
		}
	}
	
	//Method to generate an HTML parse tree.
	public void convertTreeToHTML() {
		while(currentToken < parseTree.size()) {
			//System.out.print("Converting _+" + parseTree.get(currentToken) + "+_ to ");
			convertToken(); //Run through the parse tree and convert it to an HTML tree.
			//System.out.println("_+" + parseTree.get(currentToken) + "+_");
		}
	}
	
	//Method to convert a section of the parse tree.
	//Since some of our tags 
	public void convertToken() {
		switch(parseTree.get(currentToken).toUpperCase()) {
		
		//"Begin" tags. All are unique in Lolcode.
			case LexicalTokens.DOC_BEGIN:
				convertDOC_BEGIN();
				break;
			case LexicalTokens.HEAD_BEGIN:
				convertHEAD_BEGIN();
				break;
			case LexicalTokens.COMMENT_BEGIN:
				convertCOMMENT_BEGIN();
				break;
			case LexicalTokens.PARAGRAPH_BEGIN:
				convertPARAGRAPH_BEGIN();
				break;	
			case LexicalTokens.LIST_BEGIN:
				convertLIST_BEGIN();
				break;
			case LexicalTokens.TITLE_BEGIN:
				convertTITLE_BEGIN();
				break;
			case LexicalTokens.BOLD_BEGIN:
				convertBOLD_BEGIN();
				break;
			case LexicalTokens.ITALICS_BEGIN:
				convertITALICS_BEGIN();
				break;
			case LexicalTokens.LIST_ITEM_BEGIN:
				convertLIST_ITEM_BEGIN();
				break;
			case LexicalTokens.SOUND_BEGIN:
				convertSpecial_SOUND_BEGIN();
				break;
			case LexicalTokens.VIDEO_BEGIN:
				convertVIDEO_BEGIN();
				break;
			case LexicalTokens.VARIABLE_DEFINE_BEGIN:
				convertSpecial_VARIABLE_DEFINE_BEGIN();
				break;
			case LexicalTokens.VARIABLE_USE_BEGIN:
				convertSpecial_VARIABLE_USE_BEGIN(parseTree.get(getNextNonWhiteSpaceToken()));
				break;	
				
			//"Non-begin" tags that are unique in Lolcode.
			case LexicalTokens.DOC_END:
				convertDOC_END();
				break;	
			case LexicalTokens.COMMENT_END:
				convertCOMMENT_END();
				break;
			case LexicalTokens.NEWLINE:
				convertNEWLINE();
				break;
			//case LexicalTokens.VARIABLE_DEFINE_MIDDLE:
			//	break;				
				
			//Non-unique tags that must be handled specifically.
			default:			
				if (isHeadEnd()) {
					convertHEAD_END();
				} else if(isParagraphEnd()) {
					convertPARAGRAPH_END();
				} else if(isListEnd()) {
					convertLIST_END();
				} else if(isTitleEnd()) {
					convertTITLE_END();
				} else if(isBoldEnd()) {
					convertBOLD_END();
				} else if(isItalicsEnd()) {
					convertITALICS_END();
				} else if(isListItemEnd()) {
					convertLIST_ITEM_END();
				} else if(isSoundEnd()) {
					convertSpecial_SOUND_END();
				} else if(isVideoEnd()) {
					convertVIDEO_END();
				//} else if(parseTree.get(currentToken).equals(LexicalTokens.VARIABLE_DEFINE_END)) {
			//		//Need to do.
				//} else if(parseTree.get(currentToken).equals(LexicalTokens.VARIABLE_USE_END)) {
			//		//Need to do.
				} else {
					//Do nothing, it's just text or whitespace. Simply let the counter advance.
					//For error checking:
					//System.out.println("Not-converting text: +_" + parseTree.get(currentToken) + "_+");
				}
				break; //End Default
		}//End Switch
		
		currentToken++; //Increment counter to allow us to get the next token.
	}	
	
	//Special Convert Methods
	public void convertSpecial_SOUND_BEGIN() {
		convertSOUND_BEGIN();
		 //We want to add a new member to the parse tree.
		//The new member needs to be directly after the SOUND_BEGIN member.
		currentToken = getNextNonWhiteSpaceToken();
		parseTree.add(currentToken, HTMLTokens.SOUND_SOURCE_BEGIN);
	}
	
	public void convertSpecial_SOUND_END() {
		parseTree.add(currentToken, HTMLTokens.SOUND_SOURCE_END);
		currentToken = getNextNonWhiteSpaceToken();
		convertSOUND_END();
	}
	
	public void convertSpecial_VARIABLE_DEFINE_BEGIN() {
		recordVariable();
		removeVariableDeclaration();
	}
	
	public void convertSpecial_VARIABLE_USE_BEGIN(String identifier) {
		String varValue = getVariableValue(identifier);
		removeVariableUseStatement();
		parseTree.add(currentToken, varValue);
	}
	
	//Individual Convert Methods
	//These methods modify the parse tree directly.
	//Convert methods that should enter scope.
	public void convertDOC_BEGIN() { parseTree.set(currentToken, HTMLTokens.DOC_BEGIN); enterScope(SCOPE_BUFFER); }
	public void convertHEAD_BEGIN() { parseTree.set(currentToken, HTMLTokens.HEAD_BEGIN); enterScope(SIhead); }
	public void convertPARAGRAPH_BEGIN() { parseTree.set(currentToken, HTMLTokens.PARAGRAPH_BEGIN); enterScope(SIparagraph); }
	public void convertLIST_BEGIN() { parseTree.set(currentToken, HTMLTokens.LIST_BEGIN); enterScope(SIlist); }
	public void convertTITLE_BEGIN() { parseTree.set(currentToken, HTMLTokens.TITLE_BEGIN); enterScope(SItitle); }
	public void convertBOLD_BEGIN() { parseTree.set(currentToken, HTMLTokens.BOLD_BEGIN); enterScope(SIbold); }
	public void convertITALICS_BEGIN() { parseTree.set(currentToken, HTMLTokens.ITALICS_BEGIN); enterScope(SIitalics); }
	public void convertLIST_ITEM_BEGIN() { parseTree.set(currentToken, HTMLTokens.LIST_ITEM_BEGIN); enterScope(SIlist_item); }
	public void convertSOUND_BEGIN() { parseTree.set(currentToken, HTMLTokens.SOUND_BEGIN); enterScope(SIsound); } 
	public void convertVIDEO_BEGIN() { parseTree.set(currentToken, HTMLTokens.VIDEO_BEGIN); enterScope(SIvideo); }
	
	//Convert methods that should leave scope.
	public void convertDOC_END() { parseTree.set(currentToken, HTMLTokens.DOC_END); leaveScope(); }
	public void convertHEAD_END() { parseTree.set(currentToken, HTMLTokens.HEAD_END); leaveScope(); }
	public void convertPARAGRAPH_END() { parseTree.set(currentToken, HTMLTokens.PARAGRAPH_END); leaveScope(); }
	public void convertLIST_END() { parseTree.set(currentToken, HTMLTokens.LIST_END); leaveScope(); }
	public void convertTITLE_END() { parseTree.set(currentToken, HTMLTokens.TITLE_END); leaveScope(); }
	public void convertBOLD_END() { parseTree.set(currentToken, HTMLTokens.BOLD_END); leaveScope(); }
	public void convertITALICS_END() { parseTree.set(currentToken, HTMLTokens.ITALICS_END); leaveScope(); }
	public void convertLIST_ITEM_END() { parseTree.set(currentToken, HTMLTokens.LIST_ITEM_END); leaveScope(); }
	public void convertSOUND_END() { parseTree.set(currentToken, HTMLTokens.SOUND_END); leaveScope(); }
	public void convertVIDEO_END() { parseTree.set(currentToken, HTMLTokens.VIDEO_END); leaveScope(); }
	
	//Convert methods that should not affect scope.
	public void convertNEWLINE() { parseTree.set(currentToken, HTMLTokens.NEWLINE); }
	public void convertCOMMENT_BEGIN() { parseTree.set(currentToken, HTMLTokens.COMMENT_BEGIN); }
	public void convertCOMMENT_END() { parseTree.set(currentToken, HTMLTokens.COMMENT_END); }	
	
	//Variable Methods
	//For handling scopeStack in the special way we have to.
	//Note: Variable conversion are taken care of in "Special Convert Methods".
	
	//This method should be called right after a #I HAS A tag is found.
	//This method should be called before currentToken is updated.
	//This method reads in the variable and adds it to the variable stack.
	public void recordVariable() {
		String name;
		String value = "";		  //String to hold the variable.
		int i = currentToken; //Counter to go through the variable section.
		
		name = parseTree.get(getNextNonWhiteSpaceToken());
		
		while(!parseTree.get(i).equals(LexicalTokens.VARIABLE_DEFINE_MIDDLE)) {
			i++;
		}
		i++;//<-- ^^ These statements are for incrementing to the variable value.
		
		while(!parseTree.get(i).equals(LexicalTokens.VARIABLE_DEFINE_END)) { //While there are still variable contents to be read.
			value = value + parseTree.get(i);
			i++;
		}
		scopeStack.add(scopeStackTop + 1, value);
		scopeStack.add(scopeStackTop + 2, name); //Name should be on top of value.
		scopeStackTop = scopeStackTop + 2;
	}
	
	//Method to take the variable declaration out of the parse tree.
	public void removeVariableDeclaration() {
		while(!parseTree.get(currentToken).equals(LexicalTokens.VARIABLE_DEFINE_END)) { //While we're still in the variable declaration.
			parseTree.remove(currentToken); //This will shift the rest of the declaration to the left, so we do not need to increment currentToken.
		}
		parseTree.remove(currentToken); //Remove the VARIABLE_DEFINE_END tag.
	}
	
	//Method to get the variable with the associated identifier.
	public String getVariableValue(String identifier) {
		//Start at the top of our variable list, search from back to front
		//(top to bottom) and return the first variable of the same name.
		//Have I decrement by two each time in order to skip over the
		//contents of scopeStack and only search the names.
		System.out.println("\n\n=*=*=*=*=*=*=");
		System.out.println("Searching for variable \"" + identifier + "\"");
		for(int i=scopeStackTop; i>=0; i-=2) {
			if(identifier.equals(scopeStack.get(i))) {
				System.out.println("Found varName " + identifier + " at stack index " + i + ".");
				System.out.println("Converting to " + "\"" + scopeStack.get(i-1) + "\".");
				System.out.println("=*=*=*=*=*=*=\n");
				return scopeStack.get(i-1);
			}
		}//end for
		System.err.println("Error (Static Semantic Error): Cannot find symbol: " + identifier + ".");
		System.err.println("Our scopeStack at the time of system failure is, from top to bottom:");
		for(int i=scopeStackTop; i >=0 ; i--) {
			System.err.println(scopeStack.get(i));
		}
		System.exit(-1);
		
		return ""; //This line is just so that this method will compile.
	}
	
	//Method to remove the variable use statement.
	public void removeVariableUseStatement() {
		while(!parseTree.get(currentToken).equals(LexicalTokens.VARIABLE_USE_END)) {
			parseTree.remove(currentToken);
		}
		parseTree.remove(currentToken); //Remove #MKAY
	}
	
	//Method to update the scopeStackTop to reflect the current block's scope.
	//This method fills in any blanks that may result from scopeStack not being defined.
	public void enterScope(String scopeIndicator) {
		//Add two slots of scope buffer to separate this section.
		scopeStack.add(scopeStackTop + 1, SCOPE_BUFFER);
		scopeStack.add(scopeStackTop + 2, scopeIndicator);
		//Set stack top to appropriate location.
		scopeStackTop = scopeStackTop + 2;
	}
	
	//Method to update scopeStackTop to reflect the departure from a scope block.
	public void leaveScope() {
		scopeStackTop = getMostRecentScopeIndicator() - 2;
	}
	
	//Method to compare the most recent scope indicator to String.
	//Returns true if s = SI.
	public boolean isScopeEqualTo(String s) {
		return scopeStack.get(getMostRecentScopeIndicator()).equals(s);
	}
	
	//Method to return the index of the topmost scope indicator
	public int getMostRecentScopeIndicator() {
		for(int i=scopeStackTop; i>=0; i--) {
			if(isScopeIndicator(scopeStack.get(i))) {
				return i;
			}
		}
		/*System.err.println("Error: No scope could be identified.");
		System.err.println("Unfinished Conversion Tree = ");
		for(int i=0; i<parseTree.size(); i++) {
			System.out.print(parseTree.get(i));
		}
		System.exit(-1);*/
		return -1;
	}

	/*public int getMostRecentVariableName() {
		for(int i=scopeStackTop; i>=0; i-=2) {
			if(!scopeStack.get(i).equals(SCOPE_BUFFER) && !isScopeIndicator(scopeStack.get(i))) {
				return i;
			}
		}
		System.err.println("Error: No variables have been declared.");
		System.exit(-1);
		return -1;
	}*/
	
	//Method to check if an String is a scope indicator.
	public boolean isScopeIndicator(String s) {
		return( s.equals(SIhead)
				|| s.equals(SIparagraph)
				|| s.equals(SIlist)
				|| s.equals(SItitle)
				|| s.equals(SIbold)
				|| s.equals(SIitalics)
				|| s.equals(SIlist_item)
				|| s.equals(SIsound)
				|| s.equals(SIvideo)
				);
	}
	
	//Method to get the next nonwhitespace token.
	public int getNextNonWhiteSpaceToken() {
		int i = currentToken + 1;
		
		while(parseTree.get(i).equals(LexicalTokens.SPACE)
				|| parseTree.get(i).equals(LexicalTokens.TAB)
				|| parseTree.get(i).equals(LexicalTokens.NL)) {
			i++;
		}
		return i;
	}
	
	//Duplicate End-Checker Methods
	//These methods check if an end tag is of a specific type.
	public boolean isHeadEnd() {
		return (parseTree.get(currentToken).equalsIgnoreCase(LexicalTokens.HEAD_END)
				&& isScopeEqualTo(SIhead));
	}
	public boolean isParagraphEnd() {
		return (parseTree.get(currentToken).equalsIgnoreCase(LexicalTokens.PARAGRAPH_END)
				&& isScopeEqualTo(SIparagraph));
	}
	public boolean isListEnd() {
		return (parseTree.get(currentToken).equalsIgnoreCase(LexicalTokens.LIST_END)
				&& isScopeEqualTo(SIlist));
	}
	public boolean isTitleEnd() {
		return (parseTree.get(currentToken).equalsIgnoreCase(LexicalTokens.TITLE_END)
				&& isScopeEqualTo(SItitle));
	}
	public boolean isBoldEnd() {
		return (parseTree.get(currentToken).equalsIgnoreCase(LexicalTokens.BOLD_END)
				&& isScopeEqualTo(SIbold));
	}
	public boolean isItalicsEnd() {
		return (parseTree.get(currentToken).equalsIgnoreCase(LexicalTokens.ITALICS_END)
				&& isScopeEqualTo(SIitalics));
	}
	public boolean isListItemEnd() {
		return (parseTree.get(currentToken).equalsIgnoreCase(LexicalTokens.LIST_ITEM_END)
				&& isScopeEqualTo(SIlist_item));
	}
	public boolean isSoundEnd() {
		return (parseTree.get(currentToken).equalsIgnoreCase(LexicalTokens.SOUND_END)
				&& isScopeEqualTo(SIsound));
	}
	public boolean isVideoEnd() {
		return (parseTree.get(currentToken).equalsIgnoreCase(LexicalTokens.VIDEO_END)
				&& isScopeEqualTo(SIvideo));
	}
}
